feat: add E2B cloud sandbox environment#792
Conversation
Add `E2BEnvironment`, a new environment backend that runs commands inside [E2B](https://e2b.dev) cloud sandboxes. Unlike the Docker and Modal backends, it requires no local Docker daemon — the sandbox runs entirely in the cloud. Key design decisions: - **Automatic template management**: The first time a Docker image is used, `E2BTemplateManager` converts it into a persistent E2B template via `Template.build()`. Subsequent runs reuse the cached template, so the build cost is paid only once per unique image. - **Deterministic template naming**: `_image_to_template_name()` produces a stable, collision-resistant name (sha256 8-char suffix) that stays within E2B's 63-character, alphanumeric-plus-hyphen limit. - **Thread-safe build timeout**: Template builds run in a `ThreadPoolExecutor` (not `signal.alarm`) so that the timeout works correctly when called from worker threads (e.g., parallel SWE-bench runs). - **SWE-bench integration**: `get_sb_environment()` in `swebench.py` now injects the instance image for `e2b` the same way it does for `docker` and `swerex_modal`. Changes: - `src/minisweagent/environments/extra/e2b.py` — new environment class - `src/minisweagent/environments/__init__.py` — register `"e2b"` key - `src/minisweagent/run/benchmarks/swebench.py` — inject image for e2b - `pyproject.toml` — add `e2b` optional dependency (`e2b>=1.0.0`) - `tests/environments/extra/test_e2b.py` — 18 unit tests (all passing) - `docs/` — update environments reference and README
for more information, see https://pre-commit.ci
Add a module-level _active_sandboxes set and an atexit handler (_cleanup_all_sandboxes) that kills all live sandboxes when the interpreter exits. This ensures sandboxes are cleaned up on Ctrl+C or unhandled exceptions where __del__ may not be reliably called. - __init__ adds self to _active_sandboxes after sandbox creation - stop() removes self from _active_sandboxes before calling sandbox.kill() - atexit handler iterates over a snapshot of the set to avoid mutation issues Two additional tests cover the registry and cleanup behaviour.
E2B_ACCESS_TOKEN / access_token is not recognised by the E2B SDK. Remove the config field, all call-site usages (Template.build, Sandbox.create), the serialize exclusion, and the corresponding tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
get_or_build() was short-circuiting to the else branch without consulting skip_cache, making force-rebuild impossible despite the field documenting "force-rebuild even if it already exists". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Great! Happy to include that. I'm gonna look through things in detail soon. |
|
Hello @klieret, nice to meet you. I’ve run some test executions using the setup below: source .env
# OPENAI_API_KEY=
# E2B_API_KEY=uv run mini-extra swebench \
--model openai/gpt-5-nano \
--split test \
--workers 4 --environment-class e2b --output ./results
As shown in the screenshot, the execution is working as expected. I’ve also attached the result file for reference. Please let me know if there are any additional checks or scenarios you’d like me to validate. |
SWE-bench Docker images have /testbed owned by root, but E2B sandboxes run commands as user (UID 1000) by default, causing permission denied. Add user="root" to commands.run() to match Docker behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
9159fb4 to
32d1f91
Compare
|
Awesome, let me review in detail today! |
|
(Also I sometimes announce new features/tag new contributors on twitter/linkedin, do you have an account that I should mention?) |
|
@klieret Sounds great! Here are my linkedin/X profile. https://www.linkedin.com/in/leejunyeop/ Thanks :) |
When a cached template has been deleted on E2B's servers, Template.exists() still returns True but Sandbox.create() fails with a 404. This catches the error and triggers a rebuild automatically instead of requiring manual skip_cache=True.
Previously, sandbox resources were only cleaned up via atexit handler, which would not run if the process was forcefully terminated (e.g. double Ctrl+C). Now env.stop() is called in the finally block.
for more information, see https://pre-commit.ci
|
Hi @klieret, hope you doing well. I ran a full reproduction of the E2B environment on SWE-bench Verified (test split, 500 instances) using Result: 56.40% (282 / 500) - consistent with the reference run on the dashboard. Full evaluation report and predictions : https://gist.github.com/JunYeopLee/951e669863eb19be9a03b04b2b386fc6 Command uv run mini-extra swebench \
-m openai/gpt-5-mini \
-c swebench.yaml \
-c model.model_class=minisweagent.models.litellm_response_model.LitellmResponseModel \
-c model.model_kwargs.temperature=null \
-c model.model_kwargs.text.verbosity=medium \
-c model.model_kwargs.reasoning.effort=medium \
--environment-class e2b \
-c environment.sandbox_timeout=7200 \
--subset verified \
--split test \
--workers 40 \
-o results_gpt_5_mini/Please check :D |
|
Hi @klieret , just wanted to kindly follow up on this PR. I was wondering if there are any updates, or if there is anything else I can help with to move this forward. Happy to make any changes or run additional checks if needed. |
|
Hi @JunYeopLee so sorry for the late response, there was just so much going on. Let me look into things right now |
There was a problem hiding this comment.
Pull request overview
Adds a new E2BEnvironment backend to run commands inside E2B cloud sandboxes (including template caching/building), wires it into SWE-bench runs, and documents/tests the new environment.
Changes:
- Introduce
E2BEnvironmentConfig,E2BTemplateManager, andE2BEnvironmentfor E2B-based execution (template build + cache). - Register the new
"e2b"environment key and inject per-instance SWE-bench images when usinge2b. - Add unit tests and documentation for configuring/using the E2B environment; add optional
e2bdependency extra.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/minisweagent/environments/extra/e2b.py |
New E2B sandbox environment + template management and cleanup. |
src/minisweagent/environments/__init__.py |
Registers e2b in the environment mapping. |
src/minisweagent/run/benchmarks/swebench.py |
Injects SWE-bench image for e2b; stops environments after each instance. |
pyproject.toml |
Adds e2b optional dependency extra. |
tests/environments/extra/test_e2b.py |
Adds unit tests for naming, execution behavior, serialization, and cleanup. |
docs/advanced/environments.md |
Adds e2b to the environment list. |
docs/reference/environments/e2b.md |
Adds a reference page for the E2B environment. |
README.md |
Mentions E2B as a deployable environment option. |
| def get_sb_environment(config: dict, instance: dict) -> Environment: | ||
| env_config = config.setdefault("environment", {}) | ||
| env_config["environment_class"] = env_config.get("environment_class", "docker") | ||
| image_name = get_swebench_docker_image_name(instance) | ||
| if env_config["environment_class"] in ["docker", "swerex_modal"]: | ||
| if env_config["environment_class"] in ["docker", "swerex_modal", "e2b"]: |
| from types import ModuleType | ||
| from unittest.mock import MagicMock, patch | ||
|
|
||
| import pytest | ||
|
|
||
| from minisweagent.environments.extra.e2b import ( | ||
| E2BEnvironment, | ||
| E2BEnvironmentConfig, | ||
| E2BTemplateManager, | ||
| ) | ||
| from minisweagent.exceptions import Submitted | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Helpers | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| def _make_mock_e2b() -> ModuleType: | ||
| """Return a minimal mock of the `e2b` module.""" | ||
| mock_e2b = MagicMock() | ||
| mock_e2b.Template = MagicMock() | ||
| mock_e2b.Sandbox = MagicMock() | ||
| return mock_e2b |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
for more information, see https://pre-commit.ci
| template=template_name, | ||
| timeout=self.config.sandbox_timeout, | ||
| api_key=self.config.api_key, | ||
| metadata={"user": "junyeoplee2"}, # TEMP. DO NOT MERGE |
| template=template_name, | ||
| timeout=self.config.sandbox_timeout, | ||
| api_key=self.config.api_key, | ||
| metadata={"user": "junyeoplee2"}, # TEMP. DO NOT MERGE |
There was a problem hiding this comment.
probably will also need to adapt this
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Hi @klieret Thanks for the review. I just fixed it. |
e2b's commands.run() raises CommandExitException (carrying stdout/stderr/ exit_code) on any non-zero exit. The generic except branch masked every failing command as an infrastructure error with empty output and returncode -1, hiding real command output from the agent. Detect the exit_code-carrying exception and surface the real result instead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Default/mini configs render {{system}}/{{machine}}/... under Jinja
StrictUndefined. E2BEnvironment.get_template_vars omitted these keys
(unlike docker/local), crashing those configs at agent startup. Merge
platform.uname() like the other environments.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
get_template_vars dumped the full config (api_key, registry credentials) into the Jinja prompt context. Exclude secrets there, and centralize the secret-field set so serialize() also drops registry_username (previously only api_key and registry_password were excluded). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rename E2BEnvironment.stop() to cleanup() to match the docker/singularity/ bubblewrap convention. The swebench finally block previously called env.stop() guarded by hasattr, which was a silent no-op for the default docker backend (it exposes cleanup()). Call whichever teardown method exists (cleanup or stop) so every backend's per-instance resource is released. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The stale-cache rebuild path tested 'if "404" not in str(e)', which could
match an incidental '404' inside a sandbox id or path (triggering an
expensive needless rebuild) or miss differently-worded errors. e2b formats
API errors as '{status_code}: {message}', so match the leading 404 status
code via a small testable helper instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
env teardown was the last statement of the finally block, so an exception in agent.save() or update_preds_file() would skip it and leak the cloud sandbox/container until its timeout. Move teardown into its own nested finally and extract a _teardown_environment helper so cleanup always runs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
for more information, see https://pre-commit.ci
Codecov Report❌ Patch coverage is
... and 21 files with indirect coverage changes 🚀 New features to boost your workflow:
|
|
I just saw that there's some linting issues because of not being able to import e2b? 24s |
|
Opened a small follow-up against the E2B branch for the pylint import failure: JunYeopLee#1. It adds the new E2B extra to the existing |
|
Hi @klieret, Pushed 3 commits addressing the remaining review points
Thanks! |

Summary
This PR adds
E2BEnvironment, a new environment backend that executes commands inside E2B cloud sandboxes.Key design decisions
Automatic template management
The first time a Docker image is used,
E2BTemplateManagerconverts it into a persistent E2B template viaTemplate.build(). Subsequent runs reuse the cached template, so the one-time build cost is paid only once per unique image.Deterministic template naming
_image_to_template_name()maps Docker image names to stable, collision-resistant E2B template names using a sha256 8-character suffix. The result always stays within E2B's 63-character, alphanumeric-plus-hyphen limit.Thread-safe build timeout
Template builds are wrapped in a
ThreadPoolExecutor(rather thansignal.alarm) so that the timeout works correctly when invoked from worker threads — e.g., during parallel SWE-bench evaluation runs.SWE-bench integration
get_sb_environment()inswebench.pynow injects the per-instance image fore2bthe same way it already does fordockerandswerex_modal.Changes
src/minisweagent/environments/extra/e2b.pyE2BEnvironmentConfig,E2BTemplateManager,E2BEnvironment)src/minisweagent/environments/__init__.py"e2b"key in the environment mappingsrc/minisweagent/run/benchmarks/swebench.pye2benvironment classpyproject.tomle2boptional dependency (e2b>=1.0.0)tests/environments/extra/test_e2b.pydocs/advanced/environments.mde2bentry to the environment listdocs/reference/environments/e2b.mdE2BEnvironmentREADME.mdUsage
Install the extra and set the API key:
Run SWE-bench evaluation via E2B:
mini-extra swebench \ --subset verified \ --split test \ --workers 50 \ --environment-class e2bOr in a YAML config:
Test plan
E2BEnvironmentConfig,_image_to_template_name,execute()(dict/string action, non-zero exit, exception,Submitteddetection),serialize()(structure, credential exclusion),stop()(normal, missing sandbox, exception-tolerant)